const std = @import("std");
const os = @import("../os.zig");
const sys = os.syscall;
const fd_t = sys.SysCall.fd_t;
const path_t = sys.SysCall.path_t;
const fs = os.fs;
const Inode = fs.Inode;
const Dirent = Inode.Dirent;
const File = fs.File;
const Proc = os.proc.Proc;
const log = std.log.scoped(.sysfile);

/// Fetch the corresponding struct file.
fn getfile(p: *Proc, fd: fd_t) !*File {
    if (fd >= p.files.len) return error.FileNotFound;
    return p.files[fd] orelse return error.FileNotFound;
}

pub fn dup(p: *Proc, fd: fd_t) !fd_t {
    const f = try getfile(p, fd);
    const new_fd = try p.fdalloc(f);
    _ = f.dup();
    return new_fd;
}

pub fn read(p: *Proc, fd: fd_t, buf: [*]u8, size: usize) !usize {
    const f = try getfile(p, fd);
    return f.read(@intFromPtr(buf), size);
}

pub fn write(p: *Proc, fd: fd_t, buf: [*]const u8, size: usize) !usize {
    const f = try getfile(p, fd);
    return f.write(@intFromPtr(buf), size);
}

pub fn lseek(p: *Proc, fd: fd_t, offset: fs.off_t, whence: fs.SeekMode) !usize {
    const f = try getfile(p, fd);
    return try f.seek(offset, whence);
}

pub fn close(p: *Proc, fd: fd_t) !void {
    const f = try getfile(p, fd);
    p.files[fd] = null;
    f.close();
}

test "file write then read data matches" {
    const p = os.proc.current().?;
    mkdir(p, "/tmp") catch {};

    const fd = try open(p, "/tmp/write_read.test", .{ .access = .RDWR, .create = true, .truncate = true });
    defer close(p, fd) catch {};

    const data = "hello, world";
    _ = try write(p, fd, data.ptr, data.len);
    _ = try lseek(p, fd, 0, .set);

    var buf: [32]u8 = undefined;
    const n = try read(p, fd, &buf, buf.len);

    try std.testing.expect(std.mem.eql(u8, data, buf[0..n]));
}

pub fn fstat(p: *Proc, fd: fd_t, stat: *Inode.Stat) !void {
    const f = try getfile(p, fd);
    const st = try f.stat();

    var writer = p.writer(.auto, &.{}, @ptrCast(stat));
    try writer.io_w.writeStruct(st, os.native_endian);
}

/// Create the path new as a link to the same inode as old.
pub fn link(_: *Proc, file_path: path_t, link_path: path_t) !void {
    fs.log.begin();
    defer fs.log.end();

    const inode = try fs.path.getinode(file_path);
    defer inode.put();
    {
        inode.lock();
        defer inode.unlock();

        if (inode.disk.type == .dir) return error.PermissionDenied;
        inode.disk.nlink += 1;
        inode.flush();
    }

    errdefer {
        inode.lock();
        inode.disk.nlink -= 1;
        inode.flush();
        inode.unlock();
    }

    const parent_dir, const filename = try fs.path.getparent(link_path);
    defer parent_dir.put();
    parent_dir.lock();
    defer parent_dir.unlock();

    if (parent_dir.dev != inode.dev) return error.NotSameFileSystem;
    try parent_dir.addEntry(filename, inode.inum);
}

pub fn unlink(p: *Proc, path: path_t) !void {
    fs.log.begin();
    defer fs.log.end();

    const parent_dir, const filename = try fs.path.getparent(path);
    defer parent_dir.put();

    parent_dir.lock();
    defer parent_dir.unlock();

    // Cannot unlink "." or "..".
    for (Dirent.dot_dirs) |dir| {
        if (filename.eql(dir)) return error.AccessDenied;
    }

    const inum, const offset = parent_dir.childInode(filename) orelse return error.FileNotFound;
    const inode = Inode.get(inum);
    defer inode.put();

    inode.lock();
    defer inode.unlock();

    if (inode.disk.nlink < 1) {
        log.err(
            "{s}:{} unlink {s} nlink {}",
            .{ p.name.get(), p.pid, path, inode.disk.nlink },
        );
        @panic("unlink: nlink < 1");
    }

    if (inode.disk.type == .dir and !inode.isEmpty()) return error.DirNotEmpty;

    const de: Dirent = .{};
    var reader = p.reader(.kernel, &.{}, @ptrCast(&de));
    parent_dir.write(&reader.io_r, offset, @sizeOf(Dirent)) catch @panic("unlink: writei");

    if (inode.disk.type == .dir) {
        parent_dir.disk.nlink -= 1;
        parent_dir.flush();
    }

    inode.disk.nlink -= 1;
    inode.flush();
}

pub fn symlink(p: *Proc, file_path: path_t, link_path: path_t) !void {
    fs.log.begin();
    defer fs.log.end();

    const symbol = try fs.path.create(link_path, .symlink, 0, 0);
    defer symbol.unlockput();

    var reader = p.reader(.kernel, &.{}, @ptrCast(file_path));
    try symbol.write(&reader.io_r, 0, file_path.len + 1);
}

pub fn access(_: *Proc, path: path_t) !bool {
    const inode = fs.path.getinode(path) catch return false;
    inode.put();
    return true;
}

pub fn getcwd(p: *Proc, addr: [*]u8, size: usize) !void {
    var buffer: [fs.MAXPATH]u8 = undefined;
    const cwd = try p.cwd.getpath(buffer[0 .. buffer.len - 1]);
    buffer[cwd.len] = 0;

    if (size < cwd.len + 1) return error.NameTooLong;
    var writer = p.writer(.auto, &.{}, addr);
    try writer.io_w.writeAll(buffer[0 .. cwd.len + 1]);
}

test getcwd {
    const p = os.proc.current().?;
    mkdir(p, "/tmp") catch {};
    try chdir(p, "/tmp/./../tmp");

    var buffer: [fs.MAXPATH:0]u8 = undefined;
    try getcwd(p, &buffer, buffer.len);
    const path = std.mem.sliceTo(&buffer, 0);
    try std.testing.expect(std.mem.eql(u8, path, "/tmp"));
}

pub fn open(p: *Proc, path: path_t, flag: fs.O) !fd_t {
    if (flag.access == .NONE) {
        log.err("open syscall requires read/write access mode", .{});
        return error.InvalidArgument;
    }

    fs.log.begin();
    defer fs.log.end();

    // an locked inode
    var inode: *Inode = if (flag.create)
        try fs.path.create(path, .file, 0, 0)
    else get: {
        const ip = try fs.path.getinode(path);
        ip.lock();
        break :get ip;
    };
    errdefer inode.put();
    defer inode.unlock();

    // resolve symlinks
    if (inode.disk.type == .symlink and !flag.no_follow) {
        var depth: usize = 0;
        var target: [fs.MAXPATH]u8 = undefined;

        while (inode.disk.type == .symlink) : (depth += 1) {
            if (depth > 8) return error.SymLinkLoop;
            var writer = p.writer(.kernel, &.{}, &target);
            _ = try inode.read(&writer.io_w, 0, target.len);

            const new = try fs.path.getinode(@ptrCast(&target));
            inode.unlockput();
            new.lock();
            inode = new;
        }
    }

    if (inode.disk.type == .dir and flag.access != .RDONLY) {
        return error.IsDir;
    }

    if (inode.disk.type == .device and (inode.disk.major < 0 or inode.disk.major >= os.NDEV)) {
        return error.NoDevice;
    }

    const f = try File.alloc();
    errdefer f.close();
    const fd = try p.fdalloc(f);

    f.data = switch (inode.disk.type) {
        .none => unreachable,
        .device => .{ .device = .{
            .inode = inode,
            .major = inode.disk.major,
        } },
        else => .{ .file = .{
            .inode = inode,
            .offset = 0,
        } },
    };

    f.readable = flag.access != .WRONLY;
    f.writable = flag.access == .WRONLY or flag.access == .RDWR;

    if (inode.disk.type == .file) {
        if (flag.truncate) inode.truncate();
        if (flag.append) f.data.file.offset = inode.disk.size;
    }

    return fd;
}

pub fn mkdir(_: *Proc, path: path_t) !void {
    fs.log.begin();
    defer fs.log.end();

    const dir = try fs.path.create(path, .dir, 0, 0);
    dir.unlockput();
}

pub fn mknod(_: *Proc, path: path_t, major: u16, minor: u16) !void {
    fs.log.begin();
    defer fs.log.end();

    const dev = try fs.path.create(path, .device, major, minor);
    dev.unlockput();
}

pub fn chdir(p: *Proc, path: path_t) !void {
    fs.log.begin();
    defer fs.log.end();

    const dir = try fs.path.getinode(path);
    errdefer dir.put();

    {
        dir.lock();
        defer dir.unlock();
        if (dir.disk.type != .dir) return error.NotDir;
    }

    p.cwd.put();
    p.cwd = dir;
}

pub fn pipe(p: *Proc, fds: *[2]fd_t) !void {
    const r_file, const w_file = try fs.Pipe.alloc();
    errdefer r_file.close();
    errdefer w_file.close();

    const fd0 = try p.fdalloc(r_file);
    errdefer p.files[fd0] = null;
    const fd1 = try p.fdalloc(w_file);
    errdefer p.files[fd1] = null;

    var w1 = p.writer(.auto, &.{}, @ptrCast(&fds[0]));
    try w1.io_w.writeInt(fd_t, fd0, os.native_endian);

    var w2 = p.writer(.auto, &.{}, @ptrCast(&fds[1]));
    try w2.io_w.writeInt(fd_t, fd1, os.native_endian);
}
